﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using rentit03_server.Database;

namespace rentit03_server.Business
{
    public static class UserHandler
    {
        private static readonly DataAccessObject Dao = new DataAccessObject();

        /// <summary>
        /// Create a user in the database
        /// </summary>
        /// <param name="rentitUser">Valid user</param>
        /// <param name="sessionId">Session id</param>
        /// <returns>The id for the created user</returns>
        /// <exception cref="UserHandlerException">Throws exception if user contains illegal values</exception>
        public static int CreateUser(RentitUser rentitUser, string sessionId)
        {
            SessionHandler.UserAccess(PermissionEnum.CreateUser, sessionId, true);

            var user = rentitUser.ToUser();
            user.Created = DateTime.Now;

            if (Dao.UsernameExists(user.Email))
                throw new UserHandlerException(ReturnStatus.UserValidateUsername, "Username already exists");

            IsValidUser(user);
            user.Password = SessionHandler.EncryptPassword(user.Password);
            var uid = Dao.CreateUser(user);

            if (uid > 0)
                return uid;

            throw new UserHandlerException(ReturnStatus.User, "An error occured while creating the user. The user id is " + uid + ". Please contact site admin");
        }

        /// <summary>
        /// Load a user from the database
        /// </summary>
        /// <param name="userId">User id</param>
        /// <param name="sessionId">Session id</param>
        /// <returns>User</returns>
        public static RentitUser ReadUser(int userId, string sessionId)
        {
            return Dao.ReadUser(userId).ToRentitUser(sessionId);
        }

        /// <summary>
        /// Load the user associated associated with the session id
        /// </summary>
        /// <param name="sessionId">Session id</param>
        /// <returns>User</returns>
        /// <exception cref="SessionHandlerException">Throws exception if session id is invalid</exception>
        public static RentitUser ReadUser(string sessionId)
        {
            var user = SessionHandler.GetUser(sessionId);
            return user.Id == 0 ? user.ToRentitUser("") : ReadUser(user.Id, sessionId);
        }

        /// <summary>
        /// Update a user in the database. The existing user in the database will be replaced with the new user
        /// </summary>
        /// <param name="rentitUser">New user</param>
        /// <param name="userId">User id</param>
        /// <param name="sessionId">Session id</param>
        /// <exception cref="UserHandlerException">Throws expection if user or user id is invalid</exception>
        public static List<string> EditUser(RentitUser rentitUser, int userId, string sessionId)
        {
            SessionHandler.UserAccess(PermissionEnum.EditUser, sessionId, true, userId);

            // Load user from database
            var user = Dao.ReadUser(userId);

            var changed = new List<string>();

            // Update non-empty values
            if (!String.IsNullOrWhiteSpace(rentitUser.Name) && !rentitUser.Name.Equals(user.Name))
            {
                user.Name = rentitUser.Name;
                changed.Add("name");
            }

            if (!String.IsNullOrWhiteSpace(rentitUser.Email) && !rentitUser.Email.Equals(user.Email))
            {
                user.Email = rentitUser.Email;
                changed.Add("email");
            }

            if (!String.IsNullOrWhiteSpace(rentitUser.Password) && !rentitUser.Password.Equals(user.Password))
            {
                user.Password = SessionHandler.EncryptPassword(rentitUser.Password);
                changed.Add("password");
            }

            if (!String.IsNullOrWhiteSpace(rentitUser.Address) && !rentitUser.Address.Equals(user.Address))
            {
                user.Address = rentitUser.Address;
                changed.Add("address");
            }

            if (!String.IsNullOrWhiteSpace(rentitUser.PhoneNumber) && !rentitUser.PhoneNumber.Equals(user.PhoneNumber))
            {
                user.PhoneNumber = rentitUser.PhoneNumber;
                changed.Add("phone number");
            }

            if (changed.Count > 0)
            {
                user.Updated = DateTime.Now;
                Dao.EditUser(user, userId);
            }

            return changed;
        }

        /// <summary>
        /// Delete a user in the database
        /// </summary>
        /// <param name="userId">User id</param>
        /// <param name="sessionId">Session id</param>
        /// <exception cref="UserHandlerException">Throws expection if user id is invalid</exception>
        /// <exception cref="SessionHandlerException">Throws exception if session id is invalid</exception>
        public static void DeleteUser(int userId, string sessionId)
        {
            SessionHandler.UserAccess(PermissionEnum.DeleteUser, sessionId, true, userId);

            Dao.DeleteUser(userId);
        }

        /// <summary>
        /// Get a list of users in the database
        /// </summary>
        /// <param name="sessionId">Session id</param>
        /// <returns>A list of all users in the database</returns>
        /// <exception cref="SessionHandlerException">Throws exception if session id is invalid</exception>
        public static List<RentitUser> OverviewUsers(string sessionId = "")
        {
            return OverviewUsers(new UserSearchParameter(), sessionId);
        }

        /// <summary>
        /// Get a list of users in the database fulfilling the conditions
        /// </summary>
        /// <param name="condition">The conditions for the users on the list</param>
        /// <param name="sessionId">Session id</param>
        /// <returns>A list of all users in the database fulfilling the conditions</returns>
        /// <exception cref="SessionHandlerException">Throws exception if session id is invalid</exception>
        public static List<RentitUser> OverviewUsers(UserSearchParameter condition, string sessionId)
        {
            SessionHandler.UserAccess(PermissionEnum.OverviewUser, sessionId, true);

            var users = Dao.OverviewUser(condition);
            return users.Select(u => u.ToRentitUser(sessionId)).ToList<RentitUser>();
        }

        public static int OverviewUserCount(UserSearchParameter parem, string sessionId)
        {
            return Dao.OverviewUserCount(parem);
        }

        /// <summary>
        /// Read a users credits.
        /// </summary>
        /// <param name="userId">Id of the user</param>
        /// <param name="sessionId">Session id</param>
        /// <returns>Amount of credits</returns>
        public static int ReadCredits(int userId, string sessionId)
        {
            SessionHandler.UserAccess(PermissionEnum.ReadUserCredits, sessionId, true, userId);

            return Dao.ReadCredits(userId);
        }

        /// <summary>
        /// Edit a users credits.
        /// </summary>
        /// <param name="userId">Id of the user</param>
        /// <param name="amount">The amount you want to add (add a negative amount to subtract)</param>
        /// <param name="sessionId">Session id</param>
        /// <returns>The user object</returns>
        public static RentitUser EditCredits(int userId, int amount, string sessionId)
        {
            SessionHandler.UserAccess(PermissionEnum.EditUserCredits, sessionId, true);

            if (Dao.EditCredits(userId, amount))
                return Dao.ReadUser(userId).ToRentitUser(sessionId);

            throw new UserHandlerException(ReturnStatus.UserCreditErrorUpdating, "Credit update was unsuccessful!");
        }

        /// <summary>
        /// Method for validating a user by looking at email and password.
        /// The method will throw an exception if the user cant be validated.
        /// </summary>
        private static bool IsValidUser(User user)
        {
            if (!IsValidEmail(user.Email))
                throw new UserHandlerException(ReturnStatus.UserValidateEmail, "Email must be valid");

            if (user.Email.Equals(user.Password))
                throw new UserHandlerException(ReturnStatus.UserValidatePassword, "Email and password must be different");

            if (!IsValidPassword(user.Password))
                throw new UserHandlerException(ReturnStatus.UserValidatePassword,
                                               "Password must be at least 8 characters, and must contain both upper and lowercase letters and numbers");

            return true;
        }

        /// <summary>
        /// Password validation.
        /// </summary>
        private static bool IsValidPassword(string password)
        {
            return password.Length > 7;

            //return Regex.IsMatch(password, @"^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?!.*s).*$");
        }

        /// <summary>
        /// Email validation.
        /// </summary>
        private static bool IsValidEmail(string email)
        {
            if (String.IsNullOrEmpty(email))
                throw new UserHandlerException(ReturnStatus.UserValidateEmail, "Email can't be empty");

            // Use IdnMapping class to convert Unicode domain names.
            email = Regex.Replace(email, @"(@)(.+)$", DomainMapper);

            // Return true if email is in valid e-mail format.
            return Regex.IsMatch(email,
                   @"^(?("")(""[^""]+?""@)|(([0-9a-z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-z])@))" +
                   @"(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-z][-\w]*[0-9a-z]*\.)+[a-z0-9]{2,17}))$",
                   RegexOptions.IgnoreCase);
        }

        /// <summary>
        /// Validate email domain.
        /// Code from: http://msdn.microsoft.com/en-us/library/01escwtf.aspx
        /// </summary>
        private static string DomainMapper(Match match)
        {
            // IdnMapping class with default property values.
            var idn = new IdnMapping();

            var domainName = match.Groups[2].Value;
            try
            {
                domainName = idn.GetAscii(domainName);
            }
            catch (ArgumentException)
            {
                throw new UserHandlerException(ReturnStatus.UserValidateEmail, "Wrong domain in email");
            }
            catch (NullReferenceException)
            {
                throw new UserHandlerException(ReturnStatus.UserValidateEmail, "Domain error");
            }
            return match.Groups[1].Value + domainName;
        }

        /// <summary>
        /// Convert a RentitUser to a User object that can be understood by the data layer.
        /// </summary>
        /// <param name="rentitUser">A RentitUser</param>
        /// <returns>A User object</returns>
        public static User ToUser(this RentitUser rentitUser)
        {
            if (rentitUser == null)
                return new User();

            return new User
            {
                Id = rentitUser.Id,
                Name = rentitUser.Name,
                Password = rentitUser.Password,
                Email = rentitUser.Email.ToLower(),
                Address = rentitUser.Address,
                PhoneNumber = rentitUser.PhoneNumber,
                Credits = rentitUser.Credits
            };
        }

        /// <summary>
        /// Convert a User object from the data layer to a object of the RentitUser type.
        /// The level of detail is decided by looking at permissions related to the session id.
        /// </summary>
        /// <param name="user">A User</param>
        /// <param name="sessionId">Session id</param>
        /// <returns>A RentitUser</returns>
        public static RentitUser ToRentitUser(this User user, string sessionId)
        {
            // Create a return user object and fill it with appropriate data
            var rentitUser = new RentitUser { Id = user.Id, Name = user.Name, Created = user.Created.ToString("o") };

            // Add basic user data if user has proper permissions
            if (SessionHandler.UserAccess(PermissionEnum.ReadBasicUserInfo, sessionId, false, user.Id))
            {
                rentitUser.Email = user.Email;
                rentitUser.Address = user.Address;
                rentitUser.PhoneNumber = user.PhoneNumber;
            }

            // Add user roles
            if (SessionHandler.UserAccess(PermissionEnum.ReadUserRole, sessionId, false, user.Id))
            {
                foreach (var role in user.Roles.ToList())
                {
                    rentitUser.Roles.Add(role.Name);
                }
            }

            if (SessionHandler.UserAccess(PermissionEnum.ReadUserPermissions, sessionId, false, user.Id))
            {
                foreach (var permission in SessionHandler.GetUserPermissions(user))
                    rentitUser.Permissions.Add(permission.Name);
            }

            if (SessionHandler.UserAccess(PermissionEnum.ReadAllUserInfo, sessionId, false, user.Id))
            {
                rentitUser.Credits = user.Credits;
            }

            return rentitUser;
        }
    }

    /// <summary>
    /// Business object representing af user.
    /// </summary>
    public class RentitUser
    {
        public RentitUser()
        {
            Roles = new List<string>();
            Permissions = new List<string>();
        }

        public int Id { get; set; }

        public string Name { get; set; }

        public string Password { get; set; }

        public string Email { get; set; }

        public string Address { get; set; }

        public string PhoneNumber { get; set; }

        public int Credits { get; set; }

        public object Created { get; set; }

        public List<string> Roles { get; set; }

        public List<string> Permissions { get; set; }
    }

    /// <summary>
    /// Custom exception used by the UserHandler.
    /// </summary>
    public class UserHandlerException : RentitException
    {
        public UserHandlerException(ReturnStatus status, string message) : base(status, message) { }
    }
}